﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FishServe.Build.Common;
using System.IO;
using System.Xml.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Reflection;

namespace FishServe.Build.Solution
{
    class Program
    {
        private const string _toolName = "FishServe.Build.Solution.exe";
        private const int _expectedArgCount = 4;

        static int Main(string[] args)
        {
            return new Program().Start(args);
        }

        private int Start(string[] args)
        {
            // args = new string[] { "Debug", "FishServe", @"C:\2008-12-31\CEEDT\FishServe\.", "FishServe.Client.Solution" };

            WriteInfoLine("Started");
            try
            {
                if (BuildSolution(args))
                {
                    WriteInfoLine("Exited (completed Ok)");
                    return Constants.ERRORLEVEL_SUCCESS;
                }
                else
                {
                    WriteErrorLine("Exited (failed)");
                    return Constants.ERRORLEVEL_FAIL_1;
                }
            }
            catch (Exception ex)
            {
                string exceptionString = "";
                Exception exception = ex;
                while (exception != null)
                {
                    exceptionString += String.Format("{0}\r\n{1}", ex.Message, ex.StackTrace);
                    exception = exception.InnerException;
                }

                WriteErrorLine(String.Format("Exception: {0}", exceptionString));
                WriteErrorLine("Exited (failed)");
                return Constants.ERRORLEVEL_FAIL_1;
            }            
        }

        private bool BuildSolution(string[] args)
        {
            if (args.Length != _expectedArgCount)
            {
                WriteErrorLine(String.Format("Incorrect number of arguments (expecting {0}, received {1})", _expectedArgCount, args.Length));

                for (int i = 0; i < args.Length; i++)
                    WriteInfoLine(String.Format("Args[{0}] = '{1}'", i, args[i]));

                return false;
            }

            string configurationName = args[0];
            string solutionName = args[1];
            string solutionDir = args[2].TrimEnd(new char[] { '.' });
            string releaseSetName = args[3];

            WriteDebugLine(String.Format("ConfigurationName='{0}'", configurationName));
            WriteDebugLine(String.Format("SolutionName='{0}'", solutionName));
            WriteDebugLine(String.Format("SolutionDir='{0}'", solutionDir));
            WriteDebugLine(String.Format("ReleaseSetName='{0}'", releaseSetName));

            SolutionBuildConfig buildConfig = SolutionBuildConfig.Load(Path.GetFullPath(Path.Combine(Path.Combine(solutionDir, "Solution Items"), Constants.SolutionBuildConfigFilename)));

            bool found=false;
            foreach (ReleaseSet releaseSet in buildConfig.ReleaseSets)
            {
                if (releaseSet.Name.ToLower() == releaseSetName.ToLower())
                {
                    found = true;
                    CreateReleaseSet(buildConfig, releaseSet, configurationName, solutionName, solutionDir );
                    break;
                }
            }
            if (!found)
            {
                WriteErrorLine(String.Format("Release set '{0}' not found.", releaseSetName));
                throw new Exception("Release set not found");
            }

            return true;
        }

        private void CreateReleaseSet(SolutionBuildConfig buildConfig, ReleaseSet releaseSet, string configurationName, string solutionName, string solutionDir)
        {
            foreach (ReleaseSet associatedReleaseSet in releaseSet.AssociatedReleaseSets)
            {
                bool found = false;
                foreach (ReleaseSet releaseSetSearch in buildConfig.ReleaseSets)
                {
                    if (releaseSetSearch.Name.ToLower() == associatedReleaseSet.Name.ToLower())
                    {
                        found = true;
                        CreateReleaseSet(buildConfig, releaseSetSearch, configurationName, solutionName, solutionDir);
                        associatedReleaseSet.Version = releaseSetSearch.Version;
                        associatedReleaseSet.ManifestGuid = releaseSetSearch.ManifestGuid;
                        associatedReleaseSet.Type = releaseSetSearch.Type;
                        associatedReleaseSet.Created = releaseSetSearch.Created;

                        break;
                    }
                }
                if (!found)
                {
                    WriteErrorLine(String.Format("Failed to resolve dependent release set '{0}'", associatedReleaseSet.Name));
                    throw new Exception("Dependent release set not found");
                }
            }

            string releaseManifestFilename = Path.Combine( buildConfig.DeploymentDir, String.Format( "{0}.{1}.manifest", releaseSet.Name, releaseSet.Version ));
            XDocument releaseManifestDoc = new XDocument();
            XElement releaseManifestElement = new XElement("ReleaseManifest");
            releaseManifestElement.Add( new XAttribute( "Name", releaseSet.Name ));
            releaseManifestElement.Add( new XAttribute( "Version", releaseSet.Version ));
            releaseManifestElement.Add(new XAttribute("Type", releaseSet.Type));
			releaseManifestElement.Add(new XAttribute("FileHashAlgorithm", buildConfig.FileHashAlgorithm));
            
            releaseSet.Created = DateTime.Now;
            releaseManifestElement.Add( new XAttribute( "Created", releaseSet.Created.ToString( "yyyy-MM-dd HHHH:mm:ss" ) ) );
            releaseSet.ManifestGuid = Guid.NewGuid().ToString();
            releaseManifestElement.Add( new XAttribute( "Guid", releaseSet.ManifestGuid ));
            XElement moduleManifestsElement = new XElement("ModuleManifests");
            XElement associatedReleaseManifestsElement = new XElement("AssociatedReleaseManifests");
            releaseManifestDoc.Add(releaseManifestElement);
            releaseManifestElement.Add(moduleManifestsElement);
            releaseManifestElement.Add(associatedReleaseManifestsElement);
            
            foreach (ReleaseSetModule module in releaseSet.Modules)
            {
                moduleManifestsElement.Add(GetModuleManifest(module.Name, buildConfig, configurationName, solutionName, solutionDir));
            }

            foreach (ReleaseSet associatedReleaseSet in releaseSet.AssociatedReleaseSets)
            {
                XElement element = new XElement("AssociatedReleaseManifest",
                                        new XAttribute("Name", associatedReleaseSet.Name),
                                        new XAttribute("Version", associatedReleaseSet.Version),
                                        new XAttribute("ManifestType", associatedReleaseSet.Type),
                                        new XAttribute("Created", associatedReleaseSet.Created.ToString("yyyy-MM-dd HHHH:mm:ss")),
                                        new XAttribute("ManifestGuid", associatedReleaseSet.ManifestGuid));

                associatedReleaseManifestsElement.Add(element);
            }

            XDocument signedReleaseManifestDoc = SignManifest(releaseManifestDoc);

            if (File.Exists(releaseManifestFilename))
            {
                try
                {
                    File.Delete(releaseManifestFilename);
                }
                catch (Exception)
                {
                    WriteErrorLine(String.Format("File delete failed.  Attempted to delete existing release manifest file '{0}'", releaseManifestFilename));
                    throw;
                }
            }

            try
            {
                signedReleaseManifestDoc.Save(releaseManifestFilename);
            }
            catch (Exception)
            {
                WriteErrorLine(String.Format("Failed to save release manifest '{0}'.", releaseManifestFilename));
                throw;
            }

        }

        private XDocument SignManifest(XDocument manifestDoc)
        {
            byte[] manifestBytes = UTF8Encoding.UTF8.GetBytes(manifestDoc.ToString());

            string pfxFullFilename = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), Constants.CodeSigningPfxFilename);

            if (!File.Exists(pfxFullFilename))
                throw new FileNotFoundException(String.Format("Cannot find PFX file '{0}' ({1})", Constants.CodeSigningPfxFilename, pfxFullFilename));

            X509Certificate2 certificatePfx = null;
            try
            {
                certificatePfx = new X509Certificate2(pfxFullFilename, Constants.CodeSigningPfxPassword);
            }
            catch (Exception)
            {
                WriteErrorLine(String.Format("Failed to load PFX file '{0}' ({1}):", Constants.CodeSigningPfxFilename, pfxFullFilename));
                throw;
            }

            RSACryptoServiceProvider privateKeyCsp = certificatePfx.PrivateKey as RSACryptoServiceProvider;
            byte[] signatureBytes = privateKeyCsp.SignData(manifestBytes, new SHA1CryptoServiceProvider());

            if (!privateKeyCsp.VerifyData(manifestBytes, new SHA1CryptoServiceProvider(), signatureBytes))
                throw new Exception("Manifest signature verification failed");

            string encodedManifest = Convert.ToBase64String(manifestBytes);
            string encodedSignature = Convert.ToBase64String(signatureBytes);

            XDocument signedManifestDoc = new XDocument(
                new XDeclaration("1.0", "utf-8", "yes"),
                new XComment("This file was created by a tool"),
                new XElement("SignedReleaseManifest",
                    new XElement("EncodedManifest", new XCData(encodedManifest)),
                    new XElement("EncodedSignature", new XCData(encodedSignature))
                ),
                new XText("\r\n\r\n"),
                new XComment(String.Format(" encoded manifest: \r\n\r\n{0}\r\n\r\n", manifestDoc.Root.ToString()))
            );

            return signedManifestDoc;
        }

        private XElement GetModuleManifest(string moduleName, SolutionBuildConfig buildConfig, string configurationName, string solutionName, string solutionDir)
        {
            string moduleDir = Path.Combine(Path.Combine(buildConfig.DeploymentDir, Constants.ModulesFolderName), moduleName);
            if (!Directory.Exists(moduleDir))
            {
                WriteErrorLine(String.Format("Directory not found '{0}'.  Attempting to resolve latest version of module '{1}'", moduleDir, moduleName));
                throw new Exception("Directory not found");
            }

            string[] versionDirectories = Directory.GetDirectories(moduleDir);
            if (versionDirectories.Length == 0)
            {
                WriteErrorLine(String.Format("Subdirectories not found '{0}'.  Attempting to resolve latest version of module '{1}'.", moduleDir, moduleName));
                throw new Exception("Subdirectories not found");
            }

            List<Version> versions = new List<Version>();
            foreach (string directory in versionDirectories)
            {
                string[] pathParts = directory.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar });
                Version version = ParseVersion(pathParts[pathParts.Length - 1]);
                if (version != null)
                    versions.Add(version);
            }

            if (versions.Count == 0)
            {
                WriteErrorLine(String.Format("Version subdirectories not found '{0}'.  Attempting to resolve latest version of module '{1}'.", moduleDir, moduleName));
                throw new Exception("Version subdirectories not found");
            }

            versions.Sort();

            string latestVersion = versions[versions.Count - 1].ToString();
            string moduleVersionDir = Path.Combine(moduleDir, latestVersion);
            string moduleManifestFilename = Path.Combine(moduleVersionDir, moduleName + ".manifest");
            if( !File.Exists(moduleManifestFilename ))
            {
                WriteErrorLine(String.Format("File not found '{0}'", moduleManifestFilename));
                throw new Exception("Manifest file not found");
            }
            
            XDocument signedManifestDoc = null;
            signedManifestDoc = XDocument.Load(moduleManifestFilename);

            XElement signedManifestElement = signedManifestDoc.Element("SignedManifest");

            XElement encodedManifestElement = signedManifestElement.Element("EncodedManifest");
            byte[] manifestBytes = Convert.FromBase64String(encodedManifestElement.Value);
            string manifestString = UTF8Encoding.UTF8.GetString(manifestBytes);
            XDocument manifestDoc = XDocument.Parse(manifestString);

            return manifestDoc.Root;
        }

        private Version ParseVersion(string versionString)
        {
            Version version = null;

            string[] versionParts = versionString.Split('.');
            if (versionParts.Length == 4)
            {
                foreach (string versionPart in versionParts)
                {
                    int outInt = 0;
                    if (!Int32.TryParse(versionPart, out outInt))
                        return null;
                }
                version = new Version(versionString);
            }
            return version;
        }

        private void WriteErrorLine(string line)
        {
            Console.WriteLine(String.Format("{0}: Error:  {1}", _toolName, line));
        }

        private void WriteWarningLine(string line)
        {
            Console.WriteLine(String.Format("{0}: Warning: {1}", _toolName, line));
        }

        private void WriteInfoLine(string line)
        {
            Console.WriteLine(String.Format("{0}: {1}", _toolName, line));
        }

        private void WriteDebugLine(string line)
        {
            WriteInfoLine(line);
        }
    }
}
